using System;
using System.Collections;
using System.Collections.Generic;

namespace LightCAD.Three
{
    public class PropertyMixer
    {
        #region Properties

        public IPropertyBinding binding;
        public int valueSize;
        public IList buffer;
        public int _workIndex;
        public Delegate _mixBufferRegion;
        public Delegate _mixBufferRegionAdditive;
        public Action _setIdentity;
        public int _origIndex;
        public int _addIndex;
        public double cumulativeWeight;
        public double cumulativeWeightAdditive;
        public int useCount;
        public int referenceCount;

        public int? _cacheIndex;

        #endregion

        #region constructor
        public PropertyMixer(IPropertyBinding binding, string typeName, int valueSize)
        {
            this.binding = binding;
            this.valueSize = valueSize;
            Delegate mixFunction;
            Delegate mixFunctionAdditive;
            Action setIdentity;
            // buffer layout: [ incoming | accu0 | accu1 | orig | addAccu | (optional work) ]
            //
            // interpolators can use .buffer as their .result
            // the data then goes to "incoming"
            //
            // "accu0" and "accu1" are used frame-interleaved for
            // the cumulative result and are compared to detect
            // changes
            //
            // "orig" stores the original state of the property
            //
            // "add" is used for additive cumulative results
            //
            // "work" is optional and is only present for quaternion types. It is used
            // to store intermediate quaternion multiplication results
            switch (typeName)
            {
                case "quaternion":
                    mixFunction = new Action<IList<double>, int, int, double, int?>(this._slerp);
                    mixFunctionAdditive = new Action<IList<double>, int, int, double, int>(this._slerpAdditive);
                    setIdentity = this._setAdditiveIdentityQuaternion;
                    this.buffer = new double[valueSize * 6];
                    this._workIndex = 5;
                    break;
                case "string":
                case "bool":
                    mixFunction = new Action<IList<object>, int, int, double, int>(this._select);
                    // Use the regular mix function and for additive on these types,
                    // additive is not relevant for non-numeric types
                    mixFunctionAdditive = new Action<IList<object>, int, int, double, int>(this._select);
                    setIdentity = this._setAdditiveIdentityOther;
                    if (typeName == "string")
                        this.buffer = new string[valueSize * 5];
                    else
                        this.buffer = new bool[valueSize * 5];
                    break;
                default:
                    mixFunction = new Action<IList<double>, int, int, double, int>(this._lerp);
                    mixFunctionAdditive = new Action<IList<double>, int, int, double, int>(this._lerpAdditive);
                    setIdentity = this._setAdditiveIdentityNumeric;
                    this.buffer = new double[valueSize * 5];
                    break;
            }
            this._mixBufferRegion = mixFunction;
            this._mixBufferRegionAdditive = mixFunctionAdditive;
            this._setIdentity = setIdentity;
            this._origIndex = 3;
            this._addIndex = 4;
            this.cumulativeWeight = 0;
            this.cumulativeWeightAdditive = 0;
            this.useCount = 0;
            this.referenceCount = 0;
        }
        #endregion

        #region methods
        public void accumulate(int accuIndex, double weight)
        {
            // note: happily accumulating nothing when weight = 0, the caller knows
            // the weight and shouldn"t have made the call in the first place
            var buffer = this.buffer;
            int stride = this.valueSize,
              offset = accuIndex * stride + stride;
            var currentWeight = this.cumulativeWeight;
            if (currentWeight == 0)
            {
                // accuN := incoming * weight
                for (int i = 0; i != stride; ++i)
                {
                    //buffer.SetValue(buffer.GetValue(i), offset + i);
                    buffer[offset + i] = buffer[i];
                }
                currentWeight = weight;
            }
            else
            {
                // accuN := accuN + incoming * weight
                currentWeight += weight;
                var mix = weight / currentWeight;
                try
                {
                    this._mixBufferRegion.DynamicInvoke(buffer, offset, 0, mix, stride);
                }
                catch (Exception ex)
                {

                    throw;
                }
            }
            this.cumulativeWeight = currentWeight;
        }
        public void accumulateAdditive(double weight)
        {
            var buffer = this.buffer;
            var stride = this.valueSize;
            var offset = stride * this._addIndex;
            if (this.cumulativeWeightAdditive == 0)
            {
                // add = identity
                this._setIdentity();
            }
            // add := add + incoming * weight
            this._mixBufferRegionAdditive.DynamicInvoke(buffer, offset, 0, weight, stride);
            this.cumulativeWeightAdditive += weight;
        }
        public void apply(int accuIndex)
        {
            int stride = this.valueSize;
            var buffer = this.buffer;
            var offset = accuIndex * stride + stride;
            var weight = this.cumulativeWeight;
            var weightAdditive = this.cumulativeWeightAdditive;
            var binding = this.binding;
            this.cumulativeWeight = 0;
            this.cumulativeWeightAdditive = 0;
            if (weight < 1)
            {
                // accuN := accuN + original * ( 1 - cumulativeWeight )
                var originalValueOffset = stride * this._origIndex;
                this._mixBufferRegion.DynamicInvoke(
                    buffer, offset, originalValueOffset, 1 - weight, stride);
            }
            if (weightAdditive > 0)
            {
                // accuN := accuN + additive accuN
                this._mixBufferRegionAdditive.DynamicInvoke(buffer, offset, this._addIndex * stride, 1, stride);
            }
            for (int i = stride, e = stride + stride; i != e; ++i)
            {
                if (buffer[i] != buffer[i + stride])
                {
                    // value has changed -> update scene graph
                    binding.setValue(buffer, offset);
                    break;
                }
            }
        }
        public void saveOriginalState()
        {
            var binding = this.binding;
            var buffer = this.buffer;
            var stride = this.valueSize;
            var originalValueOffset = stride * this._origIndex;
            binding.getValue(buffer, originalValueOffset);
            // accu[0..1] := orig -- initially detect changes against the original
            for (int i = stride, e = originalValueOffset; i != e; ++i)
            {
                buffer[i] = buffer[originalValueOffset + (i % stride)];
            }
            // Add to identity for additive
            this._setIdentity();
            this.cumulativeWeight = 0;
            this.cumulativeWeightAdditive = 0;
        }
        public void restoreOriginalState()
        {
            var originalValueOffset = this.valueSize * 3;
            this.binding.setValue(this.buffer, originalValueOffset);
        }
        public void _setAdditiveIdentityNumeric()
        {
            var startIndex = this._addIndex * this.valueSize;
            var endIndex = startIndex + this.valueSize;
            for (int i = startIndex; i < endIndex; i++)
            {
                this.buffer[i] = 0.0;
            }
        }
        public void _setAdditiveIdentityQuaternion()
        {
            this._setAdditiveIdentityNumeric();
            this.buffer[this._addIndex * this.valueSize + 3] = 1;
        }
        public void _setAdditiveIdentityOther()
        {
            var startIndex = this._origIndex * this.valueSize;
            var targetIndex = this._addIndex * this.valueSize;
            for (int i = 0; i < this.valueSize; i++)
            {
                this.buffer[targetIndex + i] = this.buffer[startIndex + i];
            }
        }
        public void _select(IList<object> buffer, int dstOffset, int srcOffset, double t, int stride)
        {
            if (t >= 0.5)
            {
                for (int i = 0; i != stride; ++i)
                {
                    buffer[dstOffset + i] = buffer[srcOffset + i];
                }
            }
        }
        public void _slerp(IList<double> buffer, int dstOffset, int srcOffset, double t, int? stride = null)
        {
            Quaternion.slerpFlat(buffer, dstOffset, buffer, dstOffset, buffer, srcOffset, t);
        }
        public void _slerpAdditive(IList<double> buffer, int dstOffset, int srcOffset, double t, int stride)
        {
            var workOffset = this._workIndex * stride;
            // Store result in intermediate buffer offset
            Quaternion.multiplyQuaternionsFlat(buffer, workOffset, buffer, dstOffset, buffer, srcOffset);
            // Slerp to the intermediate result
            Quaternion.slerpFlat(buffer, dstOffset, buffer, dstOffset, buffer, workOffset, t);
        }
        public void _lerp(IList<double> buffer, int dstOffset, int srcOffset, double t, int stride)
        {
            var s = 1 - t;
            for (int i = 0; i != stride; ++i)
            {
                var j = dstOffset + i;
                buffer[j] = buffer[j] * s + buffer[srcOffset + i] * t;
            }
        }
        public void _lerpAdditive(IList<double> buffer, int dstOffset, int srcOffset, double t, int stride)
        {
            for (int i = 0; i != stride; ++i)
            {
                var j = dstOffset + i;
                buffer[j] = buffer[j] + buffer[srcOffset + i] * t;
            }
        }
        #endregion

    }
}
